﻿using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace WL.Think.FileUploader
{
    public class FileUploader : IFileUploader
    {
        private static readonly FormOptions _defaultFormOptions = new FormOptions();
        private readonly ILogger<FileUploader> _logger;
        private readonly FileUploadOption _fileUploadOption;

        public FileUploader(ILoggerFactory loggerFactory, IOptionsSnapshot<FileUploadOption> options)
        {
            _logger = loggerFactory.CreateLogger<FileUploader>();
            _fileUploadOption = options.Value;
        }


        public async Task<byte[]> Save(IFormFile formFile)
        {
            return await FileHelper.ProcessFormFile(formFile, _fileUploadOption);
        }

        public async Task Save(IFormFile formFile, string targetDirectory)
        {
            var trustedFileNameForDisplay = WebUtility.HtmlEncode(formFile.Name);
            var buffer = await FileHelper.ProcessFormFile(formFile, _fileUploadOption);

            CreateDirectory(targetDirectory);
            using var targetStream = File.Create(Path.Combine(targetDirectory, trustedFileNameForDisplay));
            await targetStream.WriteAsync(buffer);
            _logger.LogInformation($"upload file {trustedFileNameForDisplay} save {targetDirectory} as {trustedFileNameForDisplay}");
        }


        public async Task<FormValueProvider> Save(HttpRequest request, Stream targetStream)
        {
            if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
            {
                throw new BadHttpRequestException($"Expected a multipart request, but got {request.ContentType}");
            }

            var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);
            var reader = new MultipartReader(boundary, request.Body);
            var section = await reader.ReadNextSectionAsync();
            var formAccumulator = new KeyValueAccumulator();

            while (section != null)
            {
                var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
                if(hasContentDispositionHeader)
                {
                    if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                    {
                        var streamedFileContent = await FileHelper.ProcessStreamFile(section, contentDisposition, _fileUploadOption);
                        await targetStream.WriteAsync(streamedFileContent);
                    }
                    else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
                    {
                        await HandleFormDataContent(formAccumulator, section, contentDisposition);
                    }
                }

                section = await reader.ReadNextSectionAsync();
            }

            return new FormValueProvider(BindingSource.Form, new FormCollection(formAccumulator.GetResults()), CultureInfo.CurrentCulture);
        }

        public async Task<FormValueProvider> Save(HttpRequest request, string targetDirectory)
        {
            if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
            {
                throw new BadHttpRequestException($"Expected a multipart request, but got {request.ContentType}");
            }

            var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);
            var reader = new MultipartReader(boundary, request.Body);
            var section = await reader.ReadNextSectionAsync();
            var formAccumulator = new KeyValueAccumulator();

            CreateDirectory(targetDirectory);

            while (section != null)
            {
                var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);

                if (hasContentDispositionHeader && !MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                {
                    var trustedFileNameForDisplay = WebUtility.HtmlEncode(contentDisposition.FileName.Value);
                    var streamedFileContent = await FileHelper.ProcessStreamFile(section, contentDisposition, _fileUploadOption);

                    using var targetStream = File.Create(Path.Combine(targetDirectory, trustedFileNameForDisplay));
                    await targetStream.WriteAsync(streamedFileContent);
                    _logger.LogInformation($"upload file {trustedFileNameForDisplay} save {targetDirectory} as {trustedFileNameForDisplay}");
                }
                section = await reader.ReadNextSectionAsync();
            }

            return new FormValueProvider(BindingSource.Form, new FormCollection(formAccumulator.GetResults()), CultureInfo.CurrentCulture);
        }


        private async Task HandleFormDataContent(KeyValueAccumulator formAccumulator, MultipartSection section, ContentDispositionHeaderValue contentDisposition)
        {
            var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name).Value;
            var encoding = GetEncoding(section);

            using var streamReader = new StreamReader(section.Body, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true);
            var value = await streamReader.ReadToEndAsync();

            if (string.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
            {
                value = string.Empty;
            }

            formAccumulator.Append(key, value);
            if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
            {
                throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
            }
        }


        private Encoding GetEncoding(MultipartSection section)
        {
            var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out var mediaType);
            return !hasMediaTypeHeader || Encoding.UTF8.Equals(mediaType.Encoding) ? Encoding.UTF8 : mediaType.Encoding;
        }


        private void CreateDirectory(string targetDirectory)
        {
            if(!string.IsNullOrWhiteSpace(targetDirectory) && !Directory.Exists(targetDirectory))
            {
                Directory.CreateDirectory(targetDirectory);
            }
        }
    }
}
